package service

import (
	"fmt"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"time"

	innner_sync "gitee.com/actor168/gitserver/pkg/sync"

	"gitee.com/actor168/gitserver/pkg/config"
	"gitee.com/actor168/gitserver/pkg/osutil"
	processed "gitee.com/actor168/gitserver/pkg/process"
	"gitee.com/actor168/gitserver/pkg/repoutil"
	"github.com/gogs/git-module"
	"github.com/pkg/errors"
	"github.com/sergi/go-diff/diffmatchpatch"
	"github.com/unknwon/com"
)

var repoWorkingPool = innner_sync.NewExclusivePool()

type CreateRepoModel struct {
	Code     string `json:"code"`
	RepoName string `json:"repoName"`
}

// CreateRepo implements Service.
func (g *gitRepository) CreateRepo(req *CreateRepoModel) *GitRespModel {
	// 调用git命令初始化git仓库
	repoPath := repoutil.RepoPath(req.Code, req.RepoName)
	if com.IsExist(repoPath) {
		return formatResp(400, fmt.Errorf("initRepository: path already exists: %s", repoPath).Error(), nil)
	}
	// Init bare new repository.
	if err := exec.Command("git", "init", "--bare", repoPath).Run(); err != nil {
		return formatResp(500, fmt.Errorf("init repository: %v", err).Error(), nil)
	} else if err = createDelegateHooks(repoPath); err != nil {
		return formatResp(500, fmt.Errorf("createDelegateHooks: %v", err).Error(), nil)
	}

	return formatResp(200, "success", repoutil.CloneLink{
		HTTP: repoutil.HTTPCloneURL(req.Code, req.RepoName),
	})
}

var hooksTpls = map[git.HookName]string{
	"pre-receive":  "#!/usr/bin/env %s\n\"%s\" hook --branch='%s' pre-receive\n",
	"update":       "#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n",
	"post-receive": "#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n",
}

func createDelegateHooks(repoPath string) (err error) {
	for _, name := range git.ServerSideHooks {
		hookPath := filepath.Join(repoPath, "hooks", string(name))
		if err = os.WriteFile(hookPath,
			[]byte(fmt.Sprintf(hooksTpls[name], config.Conf.ScriptType, config.Conf.GitBinaryPath+"gitctl", "")),
			os.ModePerm); err != nil {
			return fmt.Errorf("create delegate hook '%s': %v", hookPath, err)
		}
		break
	}
	return nil
}

func GetCommit(branch, repoPath string) (*git.Commit, error) {
	gitRepo, err := git.Open(repoPath)
	if err != nil {
		return nil, fmt.Errorf("open repository: %v", err)
	}
	return gitRepo.BranchCommit(branch)
}

type CodeCompareModel struct {
	Code     string `json:"code"`
	RepoName string `json:"repoName"`
	CommitID string `json:"commitID"`
}

// Codecomparison implements Service.
func (g *gitRepository) Codecomparison(req *CodeCompareModel) *GitRespModel {
	repoPath := repoutil.RepoPath(req.Code, req.RepoName)
	gitRepo, err := git.Open(repoPath)
	if err != nil {
		return formatResp(400, fmt.Errorf("open repository: %v", err).Error(), nil)
	}

	commit, err := gitRepo.CatFileCommit(req.CommitID)
	if err != nil {
		return formatResp(500, fmt.Sprintf("get head commit: %s", err.Error()), nil)
	}

	diff, err := gitRepo.Diff(req.CommitID, config.Conf.MaxDiffFiles, config.Conf.MaxDiffLines, config.Conf.MaxDiffLineChars,
		git.DiffOptions{Timeout: time.Duration(config.Conf.DiffTimeOut) * time.Second},
	)
	if err != nil {
		return formatResp(500, fmt.Sprintf("get diff: %s", err.Error()), nil)
	}

	change := Change{
		TotalAdditions: diff.TotalAdditions(),
		TotalDeletions: diff.TotalDeletions(),
		IsIncomplete:   diff.IsIncomplete(),
	}

	type DiffInfo struct {
		Changes Change
		Files   []*git.DiffFile
	}
	diffInfo := &DiffInfo{
		Changes: change,
		Files:   diff.Files,
	}

	parents := make([]string, commit.ParentsCount())
	for i := 0; i < commit.ParentsCount(); i++ {
		sha, err := commit.ParentID(i)
		if err != nil {
			return formatResp(500, "not found", nil)
		}
		parents[i] = sha.String()
	}

	resp := map[string]any{
		"Commits":    produceLastCommit(commit),
		"Diff":       diffInfo,
		"Parents":    parents,
		"SourcePath": config.Conf.ExternalURL + path.Join(repoutil.FullRepoName(req.Code, req.RepoName), "raw", req.CommitID),
	}
	return formatResp(200, "OK", resp)
}

type GetBranchCommitListModel struct {
	Code       string `json:"code"`
	RepoName   string `json:"repoName"`
	BranchName string `json:"branchName"`
	PageSize   int    `json:"pageSize"`
}

// HistoryCommitList implements Service.
func (g *gitRepository) HistoryCommitList(req *GetBranchCommitListModel) *GitRespModel {
	repoPath := repoutil.RepoPath(req.Code, req.RepoName)
	gitRepo, err := git.Open(repoPath)
	if err != nil {
		return formatResp(400, fmt.Errorf("open repository: %v", err).Error(), nil)
	}

	commits, err := gitRepo.Log("refs/heads/"+req.BranchName, git.LogOptions{MaxCount: req.PageSize})
	if err != nil {
		return formatResp(500, fmt.Sprintf("git log: %s", err.Error()), nil)
	}
	if err != nil {
		return formatResp(500, fmt.Sprintf("get commit error: %s", err.Error()), nil)
	}

	type Commit map[string]interface{}
	formatedCommits := make([]Commit, 0)
	for i := 0; i < len(commits); i++ {
		formatedCommits = append(formatedCommits, produceLastCommit(commits[i]))
	}
	return formatResp(200, "OK", formatedCommits)
}

type PRCommitReqModel struct {
	Code     string       `json:"code"`
	RepoName string       `json:"repoName"`
	PR       *PullRequest `json:"pr"`
	Filter   string       `json:"filter"`
}

type PullRequest struct {
	IssueId       int
	UserName      string
	Status        int
	HeadRepo      string
	BaseRepo      string
	HeadBranch    string
	BaseBranch    string
	MergeBase     string
	MergeCommitId string
	HasMerged     bool
	IsClosed      bool
}

type NumInfo struct {
	NumCommits int
	NumFiles   int
}

type PullRequestMeta struct {
	MergeBase string
	// The commits that are requested to be merged.
	Commits []*git.Commit
	// The number of files changed.
	NumFiles int
}

// PRCommits implements Service.
func (g *gitRepository) PRCommits(req *PRCommitReqModel) *GitRespModel {
	var (
		commits []*git.Commit
		numInfo NumInfo
		err     error
	)

	repoPath := repoutil.RepoPath(req.Code, req.RepoName)
	gitRepo, err := git.Open(repoPath)
	if err != nil {
		return formatResp(400, fmt.Errorf("open repository: %v", err).Error(), nil)
	}

	if req.Filter != "" {
		commit, err := gitRepo.BranchCommit(req.PR.BaseBranch)
		if err != nil {
			return formatResp(500, err.Error(), nil)
		}
		res, err := commit.SearchCommits(req.Filter)
		if err != nil {
			return formatResp(500, err.Error(), nil)
		}
		type Commit map[string]interface{}
		commits := make([]Commit, 0)
		for i := 0; i < len(commits); i++ {
			commits = append(commits, produceLastCommit(res[i]))
		}

		return formatResp(200, "OK", commits)
	}

	if req.PR.HasMerged {
		numInfo, err = g.prepareMergedViewPullInfo(req.PR)
		if err != nil {
			return formatResp(500, err.Error(), nil)
		}
		startCommit, err := gitRepo.CatFileCommit(req.PR.MergeBase)
		if err != nil {
			return formatResp(500, fmt.Sprintf("get commit of merge base:%v", err), nil)
		}
		endCommit, err := gitRepo.CatFileCommit(req.PR.MergeCommitId)
		if err != nil {
			return formatResp(500, "get merged commit", nil)
		}
		commits, err = gitRepo.RevList([]string{startCommit.ID.String() + "..." + endCommit.ID.String()})
		if err != nil {
			return formatResp(500, "list commits", nil)
		}
	} else {
		prInfo, err := g.viewPullInfo(req.PR)
		if err != nil {
			return formatResp(500, err.Error(), nil)
		}
		if prInfo == nil {
			return formatResp(500, "Not Found", nil)
		}
		commits = prInfo.Commits
		numInfo = NumInfo{
			NumCommits: len(prInfo.Commits),
			NumFiles:   prInfo.NumFiles,
		}
	}
	type Commit map[string]interface{}
	Commits := make([]Commit, 0)
	for i := 0; i < len(commits); i++ {
		Commits = append(Commits, produceLastCommit(commits[i]))
	}
	type Info struct {
		Commits []Commit
		NumInfo NumInfo
	}

	return formatResp(200, "OK", Info{
		Commits: Commits,
		NumInfo: numInfo,
	})
}

// PRFiles implements Service.
func (g *gitRepository) PRFiles(req *PRCommitReqModel) *GitRespModel {
	var (
		diffGitRepo   *git.Repository
		startCommitID string
		endCommitID   string
		numInfo       NumInfo
		err           error
	)

	if req.PR.HasMerged {
		numInfo, err = g.prepareMergedViewPullInfo(req.PR)
		if err != nil {
			return formatResp(500, err.Error(), nil)
		}

		headRepoPath := filepath.Join(config.Conf.Dir, req.PR.HeadRepo) + ".git"
		headGitRepo, err := git.Open(headRepoPath)
		if err != nil {
			return formatResp(400, fmt.Errorf("open repository: %v", err).Error(), nil)
		}

		headCommitID, err := headGitRepo.BranchCommitID(req.PR.HeadBranch)
		diffGitRepo = headGitRepo
		startCommitID = req.PR.MergeBase
		endCommitID = headCommitID
	} else {
		prInfo, err := g.viewPullInfo(req.PR)
		if err != nil {
			return formatResp(500, err.Error(), nil)
		}
		if prInfo == nil {
			return formatResp(500, "Not Found", nil)
		}
		numInfo = NumInfo{
			NumCommits: len(prInfo.Commits),
			NumFiles:   prInfo.NumFiles,
		}

		headRepoPath := filepath.Join(config.Conf.Dir, req.PR.HeadRepo) + ".git"
		headGitRepo, err := git.Open(headRepoPath)
		if err != nil {
			return formatResp(400, fmt.Errorf("open repository: %v", err).Error(), nil)
		}

		headCommitID, err := headGitRepo.BranchCommitID(req.PR.HeadBranch)
		diffGitRepo = headGitRepo
		startCommitID = prInfo.MergeBase
		endCommitID = headCommitID
	}

	diff, err := diffGitRepo.Diff(endCommitID, config.Conf.MaxDiffFiles, config.Conf.MaxDiffLines, config.Conf.MaxDiffLineChars,
		git.DiffOptions{Base: startCommitID, Timeout: time.Duration(config.Conf.DiffTimeOut) * time.Second})
	if err != nil {
		return formatResp(500, fmt.Sprintf("get diff: %s", err.Error()), nil)
	}

	type Change struct {
		TotalAdditions int
		TotalDeletions int
		IsIncomplete   bool
	}
	type DiffSection struct {
		*git.DiffSection

		initOnce sync.Once
		dmp      *diffmatchpatch.DiffMatchPatch
	}

	type DiffInfo struct {
		Changes Change
		Files   []*git.DiffFile
	}
	type data struct {
		NumInfo    NumInfo
		Diff       *DiffInfo
		SourcePath string
	}
	change := Change{
		TotalAdditions: diff.TotalAdditions(),
		TotalDeletions: diff.TotalDeletions(),
		IsIncomplete:   diff.IsIncomplete(),
	}
	diffInfo := &DiffInfo{
		Changes: change,
		Files:   diff.Files,
	}
	return formatResp(200, "OK", data{
		NumInfo: numInfo,
		Diff:    diffInfo,
	})
}

type RepoContentModel struct {
	Code       string `json:"code"`
	RepoName   string `json:"repoName"`
	BranchName string `json:"branchName"`
	SubPath    string `json:"subPath"`
}

// RepoContent implements Service.
func (g *gitRepository) RepoContent(req *RepoContentModel) *GitRespModel {
	repoPath := repoutil.RepoPath(req.Code, req.RepoName)
	rawLink := repoutil.HTTPURL(req.Code, req.RepoName) + "/raw/" + req.BranchName
	gitRepo, err := git.Open(repoPath)
	if err != nil {
		return formatResp(400, fmt.Errorf("open repository: %v", err).Error(), nil)
	}

	// Get current entry user currently looking at.
	commit, err := gitRepo.BranchCommit(req.BranchName)
	if err != nil {
		return formatResp(500, fmt.Errorf("open commit: %v", err).Error(), nil)
	}

	entry, err := commit.TreeEntry(req.SubPath)
	if err != nil {
		return formatResp(500, fmt.Errorf("open commmit entry error: %v", err).Error(), nil)
	}

	branches, err := g.getBrefBranchs(req.Code, req.RepoName)
	if err != nil {
		return formatResp(500, err.Error(), nil)
	}

	var res any
	if entry.IsTree() {
		res, err = renderDirectory(commit, req.SubPath)
		if err != nil {
			return formatResp(500, err.Error(), nil)
		}

		d := res.(TreeContent)
		return formatResp(200, "OK", map[string]any{
			"DefaultBranch":   branches[0],
			"Branchs":         branches,
			"LatestCommit":    d.LatestCommit,
			"EntryCommitInfo": d.EntryCommitInfo,
		})
	} else {
		res = renderFile(entry, req.SubPath, rawLink)
		d := res.(TreeContentFile)
		return formatResp(200, "OK", map[string]any{
			"DefaultBranch": branches[0],
			"Branchs":       branches,
			"Url":           d.Url,
			"FileSize":      d.FileSize,
			"FileName":      d.FileName,
		})
	}

}

type TreeContent struct {
	LatestCommit    map[string]any
	EntryCommitInfo []EntryCommitInfo
}

func renderDirectory(commit *git.Commit, subPath string) (any, error) {
	tree, err := commit.Subtree(subPath)
	if err != nil {
		//get subtree
		return nil, err
	}

	entries, err := tree.Entries()
	if err != nil {
		//list entries
		return nil, err
	}

	entries.Sort()

	data, err := entries.CommitsInfo(commit, git.CommitsInfoOptions{
		Path:           subPath,
		MaxConcurrency: 0,
		Timeout:        5 * time.Minute,
	})
	if err != nil {
		return nil, err
	}

	latestCommit := commit
	if len(subPath) > 0 {
		latestCommit, err = commit.CommitByPath(git.CommitByRevisionOptions{Path: subPath})
		if err != nil {
			return nil, err
		}
	}

	res := TreeContent{
		LatestCommit:    produceLastCommit(latestCommit),
		EntryCommitInfo: produceEntryCommitInfo(data),
	}
	return res, nil

}

type TreeContentFile struct {
	Url      string
	FileSize int64
	FileName string
}

func renderFile(entry *git.TreeEntry, treeLink, rawLink string) any {
	blob := entry.Blob()
	data := TreeContentFile{
		Url:      rawLink + "/" + strings.TrimLeft(treeLink, "/"),
		FileSize: blob.Size(),
		FileName: blob.Name(),
	}
	return data
}

// IsPullRequestcomparison implements Service.
func (g *gitRepository) IsPullRequestcomparison(req *PRCompareReqModel) *GitRespModel {
	headGitRepo, meta, headBranch, _, _, err := parseCompareInfo(req)
	MergeBase := meta.MergeBase
	type info struct {
		MergeBase string
		Status    int
		IssueId   int
	}
	baseBranch := req.Before
	if err != nil {
		return formatResp(500, err.Error(), nil)
	}
	patch, err := headGitRepo.DiffBinary(meta.MergeBase, headBranch)
	if err != nil {
		return formatResp(500, "get patch", nil)
	}
	index, err := getIndex(req.Code, req.RepoName)
	if err != nil {
		return formatResp(500, err.Error(), nil)
	}
	repoLink := repoutil.FullRepoName(req.Code, req.RepoName)
	if err = savePatch(index, patch, repoLink); err != nil {
		return formatResp(500, fmt.Sprintf("SavePatch err:%v", err.Error()), nil)
	}
	var status PullRequestStatus
	if status, err = testPatch(index, repoLink, baseBranch); err != nil {
		return formatResp(500, fmt.Sprintf("testPatch: %v", err), nil)
	}
	// No conflict appears after test means mergeable.
	if status == PULL_REQUEST_STATUS_CHECKING {
		status = PULL_REQUEST_STATUS_MERGEABLE
	}

	return formatResp(200, "OK", info{
		MergeBase: MergeBase,
		Status:    int(status),
		IssueId:   index,
	})
}

type PRCompareReqModel struct {
	Code     string `json:"code"`
	RepoName string `json:"repoName"`
	Before   string `json:"before"`
	After    string `json:"after"`
}

type DiffInfo struct {
	Changes Change
	Files   []*DiffFile
}

type DiffFile struct {
	*git.DiffFile
	Sections []*DiffSection
}

type DiffSection struct {
	*git.DiffSection
}

type Change struct {
	TotalAdditions int
	TotalDeletions int
	IsIncomplete   bool
}

// PullRequestcomparison implements Service.
func (g *gitRepository) PullRequestcomparison(req *PRCompareReqModel) *GitRespModel {
	headGitRepo, prInfo, headBranch, baseBranches, headBranches, err := parseCompareInfo(req)
	if err != nil {
		return formatResp(500, err.Error(), nil)
	}
	nothingToCompare, diff, afterCommitID, commits, err := prepareCompareDiff(headGitRepo, prInfo, headBranch)
	if err != nil {
		return formatResp(500, err.Error(), nil)
	}
	type Commit map[string]interface{}
	type data struct {
		IsCompare      bool
		Commit         []Commit
		Diff           *DiffInfo
		HeadBranches   []string
		BaseBranches   []string
		BeforeCommitID string
		AfterCommitID  string
		SourcePath     string
	}
	if nothingToCompare {
		// Setup information for new form.
		return formatResp(200, "OK", data{
			IsCompare:    false,
			HeadBranches: headBranches,
			BaseBranches: baseBranches,
		})
	}
	change := Change{
		TotalAdditions: diff.TotalAdditions(),
		TotalDeletions: diff.TotalDeletions(),
		IsIncomplete:   diff.IsIncomplete(),
	}
	diffInfo := &DiffInfo{
		Changes: change,
		Files:   diff.Files,
	}
	Commits := make([]Commit, 0)
	for i := 0; i < len(commits); i++ {
		Commits = append(Commits, produceLastCommit(commits[i]))
	}
	return formatResp(200, "OK", data{
		IsCompare:      true,
		HeadBranches:   headBranches,
		BaseBranches:   baseBranches,
		Commit:         Commits,
		Diff:           diffInfo,
		BeforeCommitID: prInfo.MergeBase,
		AfterCommitID:  afterCommitID,
	})
}

type MergePullRequest struct {
	Code              string        `json:"code"`
	RepoName          string        `json:"repoName"`
	BranchName        string        `json:"branchName"`
	Pull              PullRequest   `json:"pull"`
	Pulls             []PullRequest `json:"pulls"`
	MergeStyle        string        `json:"mergeStyle"`
	CommitDescription string        `json:"description"`
}

// MergePull implements Service.
func (g *gitRepository) MergePull(req *MergePullRequest) *GitRespModel {
	var (
		MergedCommitID string
		err            error
	)

	repoPath := repoutil.RepoPath(req.Code, req.RepoName)
	gitRepo, err := git.Open(repoPath)
	if err != nil {
		return formatResp(400, fmt.Errorf("open repository: %v", err).Error(), nil)
	}
	if MergedCommitID, err = g.merge(req.Pull, gitRepo, MergeStyle(req.MergeStyle), req.CommitDescription); err != nil {
		return formatResp(500, err.Error(), nil)
	}
	err = checkAndUpdateStatus(req)
	if err != nil {
		return formatResp(500, errors.Errorf("checkAndUpdateStatus err:%v", err).Error(), nil)
	}
	req.Pull.MergeCommitId = MergedCommitID
	req.Pull.HasMerged = true
	req.Pull.IsClosed = true

	return formatResp(200, "OK", map[string]any{
		"Pull":  req.Pull,
		"Pulls": req.Pulls,
	})
}

func checkAndUpdateStatus(f *MergePullRequest) error {
	for i := 0; i < len(f.Pulls); i++ {
		status, err := testPatch(f.Pulls[i].IssueId, f.Pulls[i].BaseRepo, f.Pulls[i].BaseBranch)
		if err != nil {
			return err
		}
		// No conflict appears after test means mergeable.
		if status == PULL_REQUEST_STATUS_CHECKING {
			status = PULL_REQUEST_STATUS_MERGEABLE
		}
		f.Pulls[i].Status = int(status)
	}
	return nil
}

// PatchPath returns corresponding patch file path of repository by given issue ID.
func PatchPath(index int, repoLink string) (string, error) {
	path := filepath.Join(config.Conf.Dir, repoLink) + ".git"
	return filepath.Join(path, "pulls", com.ToStr(index)+".patch"), nil
}

// testPatch checks if patch can be merged to base repository without conflict.
// FIXME: make a mechanism to clean up stable local copies.
func testPatch(index int, repoLink string, baseBranch string) (PullRequestStatus, error) {

	patchPath, err := PatchPath(index, repoLink)
	if err != nil {
		return -1, fmt.Errorf("BaseRepo.PatchPath: %v", err)
	}

	// Fast fail if patch does not exist, this assumes data is corrupted.
	if !osutil.IsFile(patchPath) {
		return -1, nil
	}

	if err := UpdateLocalCopyBranch(repoLink, baseBranch); err != nil {
		return -1, fmt.Errorf("UpdateLocalCopy [%d]: %v", 1, err)
	}

	repoWorkingPool.CheckIn(com.ToStr(repoLink))
	defer repoWorkingPool.CheckOut(com.ToStr(repoLink))

	args := []string{"apply", "--check"}
	//if pr.BaseRepo.PullsIgnoreWhitespace {
	//	args = append(args, "--ignore-whitespace")
	//}
	args = append(args, patchPath)

	Status := PULL_REQUEST_STATUS_CHECKING
	_, _, err = processed.ExecDir(-1, LocalCopyPath(repoLink),
		fmt.Sprintf("testPatch (git apply --check): %s", repoLink),
		"git", args...)
	if err != nil {
		Status = PULL_REQUEST_STATUS_CONFLICT
		return Status, nil
	}
	return Status, nil
}

func LocalCopyPath(repoLink string) string {
	return filepath.Join(config.Conf.LocalPath, "localRepo", repoLink)
}

func UpdateLocalCopyBranch(repoLink, branch string) error {
	return updateLocalCopyBranch(repoPath(repoLink), LocalCopyPath(repoLink), branch, false)
}

func repoPath(repoLink string) string {
	return filepath.Join(config.Conf.Dir, repoLink) + ".git"
}

func updateLocalCopyBranch(repoPath, localPath, branch string, isWiki bool) (err error) {
	if !osutil.IsExist(localPath) {
		// Checkout to a specific branch fails when wiki is an empty repository.
		if isWiki {
			branch = ""
		}
		if err = git.Clone(repoPath, localPath, git.CloneOptions{
			Branch:  branch,
			Timeout: time.Duration(300) * time.Second,
		}); err != nil {
			return fmt.Errorf("git clone [branch: %s]: %v", branch, err)
		}
		return nil
	}

	gitRepo, err := git.Open(localPath)
	if err != nil {
		return fmt.Errorf("open repository: %v", err)
	}

	if err = gitRepo.Fetch(git.FetchOptions{
		Prune: true,
	}); err != nil {
		return fmt.Errorf("fetch: %v", err)
	}

	if err = gitRepo.Checkout(branch); err != nil {
		return fmt.Errorf("checkout [branch: %s]: %v", branch, err)
	}

	// Reset to align with remote in case of force push.
	rev := "origin/" + branch
	if err = gitRepo.Reset(rev, git.ResetOptions{
		Hard: true,
	}); err != nil {
		return fmt.Errorf("reset [revision: %s]: %v", rev, err)
	}
	return nil
}
